#include "NcWrapper.h"
#include "NcUtil.h"
#include "OCMain/StringConverter.h"
#include "OCesium/Cartographic.h"
#include "OCesium/Ellipsoid.h"
#include "OCesium/Rectangle.h"
#include "OCesium/Transforms.h"
#include "OCesium/EllipsoidGeodesic.h"

#pragma warning(disable: 4244)

namespace OCPP
{
	NcWrapper::NcWrapper()
	{

	}

	NcWrapper::~NcWrapper()
	{
		close();
	}

	void NcWrapper::close()
	{
		CORE_LOCK_MUTEX(NcSyncFile::intanceMutex);
		if (_NcSyncFile.use_count() == 2)
		{
			NcSyncFile::instances.erase(_filName);
		}
	}

	NcVarWrapperPtr NcWrapper::findVariable(String name)
	{
		auto& ncFile = _NcSyncFile->ncFile;
		NcVar var = ncFile.getVar(name);
		if (!var.isNull())
		{
			return std::make_shared<NcVarWrapper>(var);
		}

		return nullptr;
	}

	std::shared_ptr<NcVarWrapper> NcWrapper::getVar(String name, bool splitDimZ, const VolumeOptions& options)
	{
		assert(_NcSyncFile != nullptr);

		std::shared_ptr<NcVarWrapper> ncVarWrapper;

		auto& ncFile = _NcSyncFile->ncFile;

		NcVar var = ncFile.getVar(name);

		if (!var.isNull())
		{
			ncVarWrapper = std::make_shared<NcVarWrapper>(var);
			auto size = var.getDimCount();
			for (auto i = 0; i < size; i++)
			{
				String dimName = var.getDim(i).getName();
				auto dimWrapper = getDimWrapper(dimName, options);
				bool mustBeZ = !NcUtil::isDimT(dimName, options) && i < size - 2;
				ncVarWrapper->addDimVar(dimWrapper, splitDimZ, mustBeZ, options);
			}

		}

		return ncVarWrapper;
	}

	std::shared_ptr<NcDimWrapper> NcWrapper::getDimWrapper(String name, const VolumeOptions& options)
	{
		auto it = ncDimWrapperMap.find(name);
		if (it != ncDimWrapperMap.end()) return it->second;
		auto& ncFile = _NcSyncFile->ncFile;
		auto dims = ncFile.getDims();

		auto it2 = dims.find(name);
		if (it2 != dims.end())
		{
			auto dimWrapper = _NcSyncFile->getCacheVar(name, options);
			if (dimWrapper->size() == 0)
			{
				bool filled = false;
				auto& d = it2->second;
				if (NcUtil::isDimX(name, options))
				{
					NcVar var = ncFile.getVar("lon");
					if (var.isNull()) var = ncFile.getVar("longitude");
					if (!var.isNull())
					{
						if (var.getDimCount() == 1)
						{
							dimWrapper->getFloatVec() = _NcSyncFile->readFloat(var);
						}
						else if (var.getDimCount() == 2)
						{
							size_t dLon = var.getDim(0).getSize();
							size_t dLat = var.getDim(1).getSize();

							dimWrapper->getFloatVec() = _NcSyncFile->readFloat(var, { {0,0},{dLon,1} });
						}
						IntVector origin = { var.getDimCount() };
						filled = true;
					}
				}
				else if (NcUtil::isDimY(name, options))
				{
					NcVar var = ncFile.getVar("lat");
					if (var.isNull()) var = ncFile.getVar("latitude");
					if (!var.isNull())
					{
						if (var.getDimCount() == 1)
						{
							dimWrapper->getFloatVec() = _NcSyncFile->readFloat(var);
						}
						else if (var.getDimCount() == 2)
						{
							size_t dLon = var.getDim(0).getSize();
							size_t dLat = var.getDim(1).getSize();

							dimWrapper->getFloatVec() = _NcSyncFile->readFloat(var, { {0,0},{1,dLat} });
						}
						IntVector origin = { var.getDimCount() };
						filled = true;
					}
				}
				if (!filled)
				{
					for (auto i = 0; i < d.getSize(); i++)
					{
						dimWrapper->getFloatVec().push_back(i);
					}
				}
			}
			NcVar var = ncFile.getVar(name);
			if (!var.isNull())
			{
				String uinit = "";
				if (var.getAttCount() != 0)
				{
					const auto& attrs = var.getAtts();
					if (attrs.find("long_name") != attrs.end())
					{
						try {
							const auto& at = var.getAtt("long_name");
							uinit = NcUtil::readStringAttr(at);
							dimWrapper->uinit = uinit;
						}catch(...){}
					}
				}

				auto& vecFloat = dimWrapper->getFloatVec();
				if (vecFloat.size() > 1)
				{
					dimWrapper->reverse = StringUtil::contains(uinit, "pressure")
						&& vecFloat[0] < vecFloat[1];
				}

			}

			ncDimWrapperMap.insert({ name ,dimWrapper });
			return dimWrapper;
		}

		return nullptr;

	}

	void NcWrapper::readDims()
	{

	}

	NcOutputList NcWrapper::read(String varName, int levelIndex, int timeIndex, const VolumeOptions& options)
	{
		NcOutputList ncOutputList;

		auto ncVarWrapper = getVar(varName, levelIndex > -1, options);
		if (ncVarWrapper == nullptr)
		{
			OCPP_LOG_ERROR << str_format("%s var not found.\n", varName.c_str());
			return ncOutputList;
		}

		const VolumeGridData& _varNcGridData = ncVarWrapper->_ncGridData;

		if (!_varNcGridData.isValid())
		{
			OCPP_LOG_ERROR << str_format("%s var not valid.\n", varName.c_str());
			return ncOutputList;
		}

		const auto& dimList = ncVarWrapper->getDimList();

		size_t dimSize = dimList.size();

		auto var = ncVarWrapper->_var;
		auto shape = ncVarWrapper->_shape;
		NcShape origin(shape.size(), 0);

		if (dimSize == 0)
		{
			NcOutput ncOutput(ncVarWrapper->_var, { origin,shape });
			ncOutput.copyFrom(_varNcGridData);
			ncOutput.addDimension(varName);
			ncOutputList.emplace_back(std::move(ncOutput));
		}
		else if (dimSize == 1)
		{
			shape[0] = 1;
			const auto& dim1 = dimList[0];
			for (size_t i1 = 0; i1 < dim1->size(); i1++)
			{
				auto destIndex = ncVarWrapper->hasDimTime ? timeIndex : levelIndex;
				if (destIndex > -1 && destIndex != i1)
				{
					continue;
				}

				origin[0] = i1;
				String name1 = dim1->getString(i1);
				NcOutput ncOutput(ncVarWrapper->_var, { origin,shape });
				ncOutput.copyFrom(_varNcGridData);
				ncOutput.addDimension(name1);
				ncOutput.addDimension(varName);
				ncOutputList.emplace_back(std::move(ncOutput));
			}
		}
		else if (dimSize == 2)
		{
			shape[0] = shape[1] = 1;
			const auto& dim1 = dimList[0];
			const auto& dim2 = dimList[1];
			for (size_t i1 = 0; i1 < dim1->size(); i1++)
			{
				if (timeIndex > -1 && timeIndex != i1)
				{
					continue;
				}

				origin[0] = i1;
				String name1 = dim1->getString(i1);
				for (size_t i2 = 0; i2 < dim2->size(); i2++)
				{
					if (levelIndex > -1 && levelIndex != i2)
					{
						continue;
					}
					origin[1] = i2;
					String name2 = dim2->getString(i2);
					NcOutput ncOutput(ncVarWrapper->_var, { origin,shape });
					ncOutput.copyFrom(_varNcGridData);
					ncOutput.addDimension(name1);
					ncOutput.addDimension(name2);
					ncOutput.addDimension(varName);
					ncOutputList.emplace_back(std::move(ncOutput));
				}
			}
		}
		else if (dimSize == 3)
		{
			shape[0] = shape[1] = shape[2] = 1;
			const auto& dim1 = dimList[0];
			const auto& dim2 = dimList[1];
			const auto& dim3 = dimList[2];
			for (size_t i1 = 0; i1 < dim1->size(); i1++)
			{
				if (timeIndex > -1 && timeIndex != i1)
				{
					continue;
				}

				origin[0] = i1;
				String name1 = dim1->getString(i1);
				for (size_t i2 = 0; i2 < dim2->size(); i2++)
				{
					origin[1] = i2;
					String name2 = dim2->getString(i2);
					for (size_t i3 = 0; i3 < dim3->size(); i3++)
					{
						if (levelIndex > -1 && levelIndex != i3)
						{
							continue;
						}
						origin[2] = i3;
						String name3 = dim3->getString(i3);
						NcOutput ncOutput(ncVarWrapper->_var, { origin,shape });
						ncOutput.copyFrom(_varNcGridData);
						ncOutput.addDimension(name1);
						ncOutput.addDimension(name2);
						ncOutput.addDimension(name3);
						ncOutput.addDimension(varName);
						ncOutputList.emplace_back(std::move(ncOutput));
					}
				}
			}
		}
		else if (dimSize == 4)
		{
			shape[0] = shape[1] = shape[2] = shape[3] = 1;
			const auto& dim1 = dimList[0];
			const auto& dim2 = dimList[1];
			const auto& dim3 = dimList[2];
			const auto& dim4 = dimList[3];
			for (size_t i1 = 0; i1 < dim1->size(); i1++)
			{
				origin[0] = i1;
				String name1 = dim1->getString(i1);
				for (size_t i2 = 0; i2 < dim2->size(); i2++)
				{
					origin[1] = i2;
					String name2 = dim2->getString(i2);
					for (size_t i3 = 0; i3 < dim3->size(); i3++)
					{
						origin[2] = i3;
						String name3 = dim3->getString(i3);
						for (size_t i4 = 0; i4 < dim4->size(); i4++)
						{
							origin[3] = i4;
							String name4 = dim4->getString(i4);
							NcOutput ncOutput(ncVarWrapper->_var, { origin,shape });
							ncOutput.copyFrom(_varNcGridData);
							ncOutput.addDimension(name1);
							ncOutput.addDimension(name2);
							ncOutput.addDimension(name3);
							ncOutput.addDimension(name4);
							ncOutput.addDimension(varName);
							ncOutputList.emplace_back(std::move(ncOutput));
						}
					}
				}
			}
		}

		return ncOutputList;
	}

	FloatVector NcWrapper::readFloat(const NcOutput& ncOutput)
	{
		return _NcSyncFile->readFloat(ncOutput._var, ncOutput._readIndex);
	}

	void NcWrapper::fixByAttr(NcOutput& ncOutput, const VolumeOptions& options)
	{
		//fix extent from global attrs

		// OriginX,OriginY
		// scale_factor,add_offset
		auto& extent = ncOutput.getExtent();
		if (extent.xMin < -180.0 || extent.yMin < -90.0
			|| extent.xMax > 180.0 || extent.yMax > 90.0)
		{
			auto& ncFile = _NcSyncFile->ncFile;
			auto originX = ncFile.getAtt("OriginX");
			auto originY = ncFile.getAtt("OriginY");
			
			float orX = NcUtil::readFloatAttr(originX, 0.0f);
			float orY = NcUtil::readFloatAttr(originY, 0.0f);

			if (orX != orY)
			{
				Cesium::EllipsoidGeodesic eGeodesic(Cesium::Cartographic(0.0, orY),
					Cesium::Cartographic(Cesium::CesiumMath::PI_OVER_TWO, orY), Cesium::Ellipsoid::WGS84);
				auto meterPerDegree = eGeodesic.getDistance() * 4.0 / 360.0;
				extent.xMin = orX + extent.xMin * 1e+3 / meterPerDegree;
				extent.xMax = orX + extent.xMax * 1e+3 / meterPerDegree;

				{
					Cesium::EllipsoidGeodesic eGeodesic(Cesium::Cartographic(orX, 0.0),
						Cesium::Cartographic(orX, Cesium::CesiumMath::PI_OVER_TWO), Cesium::Ellipsoid::WGS84);
					meterPerDegree = eGeodesic.getDistance() * 2.0 / 180.0f;
				}
				extent.yMin = orY + extent.yMin * 1e+3 / meterPerDegree;
				extent.yMax = orY + extent.yMax * 1e+3 / meterPerDegree;
			}
		}
	}

	void NcWrapper::extract(String varName, int levelIndex, int timeIndex, const VolumeOptions& options)
	{
		NcOutputList ncOutputList = read(varName, levelIndex, timeIndex, options);
		for (auto& ncOutput : ncOutputList)
		{
			if (ncOutput.isValid())
			{
				ncOutput.setType(VolumeGridData::Type_Float);
				ncOutput._vecFloat = readFloat(ncOutput);
				fixByAttr(ncOutput, options);
				ncOutput.ClipIf(options);
				options.getWriter().write(ncOutput, options);
			}
		}
	}

	void NcWrapper::print()
	{
		//const auto& ncFile = _NcSyncFile->ncFile;
		//float fillValue;
		//float missingValue;

		//std::cout << "variabls:{\n";
		//auto vars = ncFile.getVars();
		//for (auto i : vars)
		//{
		//	String name = i.first;
		//	std::cout << "\t" << name << " \t";

		//	const NcVar& ncVar = i.second;

		//	std::vector<NcDim> dims = ncVar.getDims();
		//	for (size_t i = 0; i < dims.size(); i++)
		//	{
		//		const NcDim& d = dims[i];
		//		std::cout << d.getName() << "(" << d.getSize() << ")";
		//		if (i < dims.size() - 1)
		//			std::cout << ",";
		//	}

		//	std::cout << "\n";
		//}
		//std::cout << "}\n";

		//std::cout << "globals attributes:{\n";
		//auto attrs = ncFile.getAtts();
		//for (auto attr : attrs)
		//{
		//	String name = attr.first;
		//	std::cout << "\t" << name << "\n";
		//}
		//std::cout << "}\n";
		OCPP_LOG_INFO << getVars().toStyledString() << "\n";
	}

	Json::Value NcWrapper::getVars()
	{
		Json::Value jsonObj;

		const auto& ncFile = _NcSyncFile->ncFile;

		// dimensions
		Json::Value dimensionsObj;
		auto dimensions = ncFile.getDims();
		for (auto [first, second] : dimensions)
		{
			dimensionsObj[first] = (int)second.getSize();
		}
		jsonObj["dimensions"] = dimensionsObj;

		auto vars = ncFile.getVars();
		Json::Value varsObj;
		for (auto i : vars)
		{
			String name = i.first;
			const NcVar& ncVar = i.second;

			if (dimensions.find(name) != dimensions.end()) continue;
			std::vector<NcDim> dims = ncVar.getDims();
			if (dims.size() < 2) continue;

			Json::Value varObj;
			varObj["name"] = name;

			Json::Value levelObj;
			Json::Value timeObj;

			for (size_t i = 0; i < dims.size() - 2; i++)
			{
				const NcDim& d = dims[i];
				auto size = d.getSize();
				if (size == 0) continue;
				String dimName = d.getName();
				VolumeOptions defalutOptions = VolumeOptions({});
				if (NcUtil::isDimT(dimName, defalutOptions))
				{
					auto dimWrapper = getDimWrapper(d.getName(), defalutOptions);
					for (auto s = 0; s < dimWrapper->size(); s++)
					{
						timeObj.append(dimWrapper->getString(s));
					}
				}
				else if (NcUtil::isDimZ(dimName, defalutOptions))
				{
					auto dimWrapper = getDimWrapper(d.getName(), defalutOptions);
					for (auto s = 0; s < size; s++)
					{
						levelObj.append(dimWrapper->getString(s));
					}
				}
				else
				{
					auto dimWrapper = getDimWrapper(d.getName(), defalutOptions);
					for (auto s = 0; s < size; s++)
					{
						levelObj.append(dimWrapper->getString(s));
					}
				}
			}

			if (levelObj.empty()) levelObj.append("0");
			if (timeObj.empty()) timeObj.append("unknown");

			varObj["level"] = levelObj;
			varObj["time"] = timeObj;


			varsObj.append(varObj);
		}

		auto attrs = ncFile.getAtts();
		Json::Value attrsObj;
		for (auto& [first, second] : attrs)
		{
			String name = first;
			auto type = second.getType();
			if (type == netCDF::NcType::nc_FLOAT)
			{
				float value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_DOUBLE)
			{
				double value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_INT)
			{
				int value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_UINT)
			{
				uint32 value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_INT64)
			{
				int64 value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_UINT64)
			{
				uint64 value;
				second.getValues(&value);
				attrsObj[name] = value;
			}
			else if (type == netCDF::NcType::nc_STRING)
			{
				String value;
				try {
					second.getValues(value);
					attrsObj[name] = value;
				}catch(...){}

				if (value.empty())
				{
					try {
						char* buffer = NULL;
						second.getValues(&buffer);
						value = String(buffer);
					}
					catch (...) {}
				}

				attrsObj[name] = String(value);
			}
			else if (type == netCDF::NcType::nc_CHAR)
			{
				//char value;
				//second.getValues(&value);
				//attrsObj[name] = (int)value;
			}
			else
			{
				OCPP_LOG_INFO << type.getName() << " unimplement type\n";
			}
		}

		jsonObj["attributes"] = attrsObj;
		jsonObj["variables"] = varsObj;
		return jsonObj;
	}

	bool NcWrapper::open(String fileName)
	{
		{
			CORE_LOCK_MUTEX(NcSyncFile::intanceMutex);
			_filName = fileName;
			auto it = NcSyncFile::instances.find(fileName);
			if (it != NcSyncFile::instances.end())
			{
				_NcSyncFile = it->second;
				return true;
			}

			try {
				_NcSyncFile = std::make_shared<NcSyncFile>();
				_NcSyncFile->ncFile.open(fileName, NcFile::read);
				if (!_NcSyncFile->ncFile.isNull()) {
					NcSyncFile::instances[fileName] = _NcSyncFile;
					return true;
				}
			}
			catch (exceptions::NcException& e) {
				OCPP_LOG_ERROR << fileName << "\t" << e.what() << "\n";
			}
			catch (...) {
				OCPP_LOG_ERROR << "unknow error.\n";
			}
		}

		_filName = "";
		_NcSyncFile = nullptr;
		return false;
	}

}
